package com.googlecode.vmock.matchers.ranking;

import java.util.HashMap;
import java.util.Map;

import com.googlecode.vmock.exceptions.MatcherRankingAlreadyTakenException;
import com.googlecode.vmock.exceptions.NoMatcherRankingException;
import com.googlecode.vmock.matchers.CompareMatcher;
import com.googlecode.vmock.matchers.DisjunctionMatcher;
import com.googlecode.vmock.matchers.EqualsMatcher;
import com.googlecode.vmock.matchers.Matcher;
import com.googlecode.vmock.matchers.TypeMatcher;
import com.googlecode.vmock.utils.Validate;

/**
 * Ranking of matchers. This class decides the execution order of matchers when searching for a match against parameter values of a replayed call.
 * 
 * @author marcin.jagus
 */
public final class MatcherRanking {

    /**
     * Maps a priority to a certain {@link Matcher} type.
     */
    private static final Map<Integer, Class<? extends Matcher>> RANKING = new HashMap<Integer, Class<? extends Matcher>>();

    /**
     * Maps a certain {@link Matcher} type to a priority.
     */
    private static final Map<Class<? extends Matcher>, Integer> INVERSE_RANKING = new HashMap<Class<? extends Matcher>, Integer>();

    /**
     * Default ranking of a {@link EqualsMatcher}.
     */
    public static final Integer EQUALS_MATCHER_DEFAULT_RANKING = 0;

    /**
     * Default ranking of a {@link DisjunctionMatcher}.
     */
    public static final Integer DISJUNCTION_MATCHER_DEFAULT_RANKING = 100;

    /**
     * Default ranking of a {@link CompareMatcher}.
     */
    public static final Integer COMPARE_MATCHER_DEFAULT_RANKING = 200;

    /**
     * Default ranking of a {@link TypeMatcher}.
     */
    public static final Integer TYPE_MATCHER_DEFAULT_RANKING = 300;

    /**
     * The lowest currently known ranking value.
     */
    private static Integer MIN_RANKING = 0;

    /**
     * The highest currently known ranking value.
     */
    private static Integer MAX_RANKING = 0;

    private MatcherRanking() {
    }

    /**
     * Returns a ranking value of a given {@link Matcher} type. If a matcher is not registered in the {@link MatcherRanking}, then <code>null</code>
     * will be returned.
     * 
     * @param matcherClass Given {@link Matcher} type
     * @return Ranking value of a given {@link Matcher} type or <code>null</code>
     */
    public static Integer getRanking(Class<? extends Matcher> matcherClass) {
        Validate.notNull(matcherClass, "matcherClass == null");

        return INVERSE_RANKING.get(matcherClass);
    }

    /**
     * Sets a new priority for a given {@link Matcher} type. End-user can also use this method to change the priority of already existing matchers.
     * Adding a new type with an already used priority will result in {@link MatcherRankingAlreadyTakenException}.
     * <p>
     * New priority value cannot be a negative number.
     * 
     * @param matcherClass Type of a matcher
     * @param rankingValue Priority of a given type
     */
    public static void setRanking(Class<? extends Matcher> matcherClass, Integer rankingValue) {
        Validate.notNull(matcherClass, "matcherClass == null");
        Validate.notNull(rankingValue, "rankingValue == null");
        Validate.isTrue(rankingValue >= 0, "rankingValue cannot be negative.");

        if (RANKING.containsKey(rankingValue)) {
            Class<? extends Matcher> clazz = RANKING.get(rankingValue);
            throw new MatcherRankingAlreadyTakenException("Ranking of " + rankingValue + " is alreay taken by " + clazz);
        }

        RANKING.put(rankingValue, matcherClass);
        INVERSE_RANKING.put(matcherClass, rankingValue);

        if (rankingValue < MIN_RANKING) {
            MIN_RANKING = rankingValue;
        } else if (rankingValue > MAX_RANKING) {
            MAX_RANKING = rankingValue;
        }
    }

    /**
     * Compares {@link Matcher} types. Throws {@link NoMatcherRankingException} if one of the types is not registered in this {@link MatcherRanking}.
     * 
     * @param a {@link Matcher} type
     * @param b {@link Matcher} type
     * @return Less than zero if type 'a' has lower ranking than type 'b'. More than zero if type 'b' has lower ranking than type 'a'. Zero if both
     *         types have the same ranking, meaning they are the same type.
     */
    public static int compare(Class<? extends Matcher> a, Class<? extends Matcher> b) {
        Integer aRanking = INVERSE_RANKING.get(a);
        Integer bRanking = INVERSE_RANKING.get(b);

        if (aRanking == null) {
            throw throwNoRankingException(a);
        } else if (bRanking == null) {
            throw throwNoRankingException(b);
        } else {
            return aRanking.compareTo(bRanking);
        }
    }

    /**
     * @return The lowest currently known ranking value
     */
    public static Integer getMinRanking() {
        return MIN_RANKING;
    }

    /**
     * @return The highest currently known ranking value
     */
    public static Integer getMaxRanking() {
        return MAX_RANKING;
    }

    /**
     * Throws {@link NoMatcherRankingException} for a given {@link Matcher} type.
     * 
     * @param matcherClass Given {@link Matcher} type
     * @return Nothing. Always throws exception.
     */
    private static NoMatcherRankingException throwNoRankingException(Class<? extends Matcher> matcherClass) {
        throw new NoMatcherRankingException("Matcher " + matcherClass
                + " does not have a ranking. Set one by using addMatcher method of the MatcherRanking class.");
    }
}
